/*
 * BaseRemoteObjectsRegistry.java   0.0.1    Nov 17, 2009
 * 
 * Copyright (c) 2009 mentalsmash.org
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * The use of the Apache License does not indicate that this project is
 * affiliated with the Apache Software Foundation.
 */
package org.mentalsmash.tazio.net.ril.impl;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.mentalsmash.documentation.Concurrency;
import org.mentalsmash.tazio.net.ril.RemoteMethod;
import org.mentalsmash.tazio.net.ril.RemoteObject;
import org.mentalsmash.tazio.net.ril.RemoteObjectProxy;
import org.mentalsmash.tazio.net.ril.RemoteObjectsRegistry;
import org.mentalsmash.tazio.net.ril.exceptions.NoSuchRemoteObjectException;
import org.mentalsmash.tazio.net.ril.exceptions.OIDAlreadyInUseException;
import org.mentalsmash.tazio.transparency.interfaces.TazioNetwork;
import org.mentalsmash.tazio.commons.TazioRuntimeException;
import org.mentalsmash.tazio.commons.identifiers.NID;
import org.mentalsmash.tazio.commons.identifiers.OID;
import org.mentalsmash.tazio.commons.timer.MonitorTimePerformances;
import org.mentalsmash.tazio.commons.timer.TimingsLog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Inject;
import com.google.inject.name.Named;

/**
 * @version 0.0.1 Nov 17, 2009
 * @author Andrea Sorbini <as@mentalsmash.org>
 * 
 */
@Concurrency(concurrentAccess = true, specification = "Access to the entries set is"
	+ "synchronized in all methods. Moreover invocations of methods on instances and removals of "
	+ "the entries containing them are serialized by a wait/notify protocol and two "
	+ "\"condition variables\" that help avoiding the two kinds of operations from happening "
	+ "concurrently (note: in this implementation, the first type of action to start gets priority"
	+ "over the other one, e.g. invocations don't block other invocations and until there's at least "
	+ "one invocation taking place no removal is allowed to proceed - this might bring to starvation"
	+ "for blocked threads and might be worthy investigating if problems arise). "
	+ "Finally access to the MBean-related status is synchronized on a specific "
	+ "lock (mbeanLock) to avoid inconsistent updates (which couldn't really happen now, "
	+ "but still... it might in the future :D)")
class BaseRemoteObjectsRegistry implements RemoteObjectsRegistry,
	BaseRemoteObjectsRegistryMBean {
    private static final Logger log = LoggerFactory
	    .getLogger(RemoteObjectsRegistry.class);

    private Map<OID, RegistryEntry> entries = new HashMap<OID, RegistryEntry>();

    // private final RemoteInvoker invoker;

    private final Object removalLock = new Object();
    private int invoking = 0;
    private int removing = 0;

    // MBean-related fields
    private final Object mbeanLock = new Object();
    private int executedInvocations = 0;

    private final ProxyFixer fixer;

    private final String name;
    private final TimingsLog tlog;
    public static final String TLOG_KEY_INVOKE = BaseRemoteObjectsRegistry.class.getName() + "#" + "invokeMethod";
    private ObjectName mbeanName;
    public ObjectName getMBeanName(){
	return this.mbeanName;
    }
    
    private final NID hostOID;

    /**
	 * 
	 */
    @Inject
    BaseRemoteObjectsRegistry(ProxyFixer fixer,
	    @Named(RILBaseImplModule.REGISTRY_NAME) String name, @Named("node_id") NID hostOID, TimingsLog tlog) {
	// this.invoker = invoker;
	// this.invoker.setRegistry(this);
	this.fixer = fixer;
	this.name = name;
	this.hostOID = hostOID;
	this.tlog = tlog;
    }

    /**
     * Registers a new instance for remote method invocation with the
     * {@link OID} provided.
     * 
     * @param oid
     *            the {@link OID} that will identify the registered instance
     * @param obj
     *            the instance to register
     * @throws OIDAlreadyInUseException
     *             if the supplied OID is already being used by some other
     *             instance in the local registry
     */
    @Override
    public void registerObject(OID oid, Object obj)
	    throws OIDAlreadyInUseException {

	synchronized (this.entries) {
	    if (!this.hasObject(oid)) {
		RegistryEntry entry = new RegistryEntry(obj, oid, this.fixer);
		this.entries.put(oid, entry);

		log.debug("New object registered: " + entry);
	    } else {
		log.warn("OID " + oid + "already in use");
		throw new OIDAlreadyInUseException(oid);
	    }
	}
    }

    private RegistryEntry getEntry(OID oid) throws NoSuchRemoteObjectException {
	synchronized (this.entries) {
	    RegistryEntry entry = this.entries.get(oid);

	    if (entry != null)
		return entry;
	    else
		throw new NoSuchRemoteObjectException(oid);
	}
    }

    /**
     * Retrieves a {@link RemoteObject} proxy for the instance identified by the
     * supplied OID.
     * 
     * @param oid
     *            an OID identifying an instance registered in the local
     *            registry.
     * @return a {@link RemoteObject} referencing the local instance identified
     *         by the supplied OID
     * @throws NoSuchRemoteObjectException
     *             if no instance in the local registry is identified by the
     *             supplied OID;
     */
    @Override
    public RemoteObject getRemoteObject(OID oid)
	    throws NoSuchRemoteObjectException {
	if (oid == null) {
	    throw new IllegalArgumentException("null oid");
	}

	synchronized (this.entries) {
	    RegistryEntry e = this.getEntry(oid);

	    if (e != null) {
		return e.getRemoteObject();
	    } else {
		log.warn("Unknown remote object requested :" + oid);
		throw new NoSuchRemoteObjectException(oid);
	    }
	}
    }

    /**
     * Checks whether an instance identified by the provided {@link OID} is
     * present in the local registry or not.
     * 
     * @param oid
     *            an OID identifying an instance registered in the local
     *            registry.
     * @return true if an instance with that OID has been registered in the
     *         local registry
     */
    @Override
    public boolean hasObject(OID oid) {
	if (oid == null)
	    return false;

	synchronized (this.entries) {
	    try {
		// try to retrieve an entry with the supplied oid;
		this.getEntry(oid);
		return true;
	    } catch (NoSuchRemoteObjectException e1) {
		return false;
	    }

	}
    }

    /**
     * Removes the instance identified by the passed OID from the ones available
     * for remote method invocation from other hosts.
     * 
     * @param oid
     *            an OID identifying an instance registered in the local
     *            registry.
     * @throws NoSuchRemoteObjectException
     *             if no instance was found for the supplied OID.
     */
    @Override
    public void unregisterObject(OID oid) throws NoSuchRemoteObjectException {
	synchronized (this.removalLock) {
	    while (this.invoking > 0) {
		try {
		    // log.debug("Waiting for " + this.invoking
		    // + " invocation to complete");
		    this.removalLock.wait();
		} catch (InterruptedException e) {

		}
	    }

	    this.removing++;
	    // log.debug("Proceeding with removal (current: " + this.removing
	    // + ")");
	}

	synchronized (this.entries) {
	    try {

		RegistryEntry toRemove = this.getEntry(oid);

		if (toRemove != null) {
		    this.entries.remove(toRemove);

		    log.trace("Unregistered object : " + toRemove);

		}
	    } finally {
		synchronized (this.removalLock) {
		    this.removing--;
		    // log.debug("Removal completed, notifying (current: "
		    // + this.removing + ")");
		    this.removalLock.notifyAll();
		}
	    }
	}
    }

    /**
     * 
     * @return an array containing {@link RemoteObject}s for all the instances
     *         registered in the local registry.
     */
    @Override
    public RemoteObject[] getRegisteredObjects() {
	ArrayList<RemoteObject> objects = new ArrayList<RemoteObject>();

	synchronized (this.entries) {
	    for (RegistryEntry entry : this.entries.values()) {
		objects.add(entry.getRemoteObject());
	    }

	    return objects.toArray(new RemoteObject[0]);
	}
    }

    /**
     * 
     * @return an array containing the {@link OID}s of all the instances
     *         registered in the local registry.
     */
    @Override
    public OID[] getRegisteredOIDs() {
//	ArrayList<OID> oids = new ArrayList<OID>();
//
//	synchronized (this.entries) {
//	    for (RegistryEntry entry : this.entries.values()) {
//		oids.add(entry.getOID());
//	    }
//
//	    return oids.toArray(new OID[0]);
//	}
	
	synchronized (this.entries) {
	    return this.entries.keySet().toArray(new OID[0]);
	}
    }

    /**
     * Checks is an instance has been registered for remote method invocation in
     * the local registry and return its OID if that's the case.
     * 
     * @param obj
     *            an Object instance
     * @return the OID which the instance has been registered with or null if
     *         the supplied instance hasn't been made available to remote method
     *         invocation.
     */
    @Override
    public OID getOID(Object obj) {
	synchronized (this.entries) {
	    for (RegistryEntry entry : this.entries.values()) {
		if (entry.getObject().equals(obj)) {
		    return entry.getOID();
		}
	    }

	    return null;
	}
    }

    /**
     * Invokes a method on the registered instance identified by the passed OID
     * and with the supplied arguments. If any exception is raised during this
     * process, it is returned as the method's return value.
     * 
     * @param oid
     *            an OID identifying a local instance
     * @param rm
     *            a {@link RemoteMethod} identifying the method to invoke
     * @param args
     *            the arguments for the method
     * @return the method's invocation result value or the exception thrown by
     *         it
     */
    @MonitorTimePerformances
    @Override
    public Object invokeMethod(OID oid, RemoteMethod rm, Object[] args)
	    throws NoSuchRemoteObjectException {
	Object result = null;

	if (rm == null) {
	    throw new IllegalArgumentException("null method supplied to invoke");
	} else if (oid == null) {
	    throw new IllegalArgumentException(
		    "null oid supplied for method invocation");
	}

	synchronized (this.removalLock) {
	    while (this.removing > 0) {
		try {
		    // log.debug("Waiting for " + this.removing
		    // + " removal to complete");
		    this.removalLock.wait();
		} catch (InterruptedException e) {

		}
	    }

	    this.invoking++;

	    // log.debug("Proceeding with invocation " + (invocSeqNum)
	    // + " (current: " + this.invoking + ")");
	}

	try {
	    RegistryEntry entry = this.getEntry(oid);

	    Object obj = entry.getObject();

	    if (!entry.hasMethod(rm)) {
		throw new IllegalArgumentException("method " + rm
			+ " not found on object " + oid);
	    }

	    if (!rm.checkArgs(args)) {
		throw new IllegalArgumentException(
			"Invalid arguments for method " + rm);
	    }

	    Method method = rm.getMethod();

	    result = method.invoke(obj, args);

	    // log.debug("Invocation's outcome: "+result);
	} catch (InvocationTargetException ex) {
	    result = ex.getCause();

	    if (result == null) {
		log.error("An exception has been thrown "
			+ "during method invocation but "
			+ "apparently with a null cause");
		throw new TazioRuntimeException(ex);
	    }

	    log.warn("Method "+rm.getName()+" invocation raised an exception : "
		    + result.getClass().getCanonicalName() + " - message: "
		    + ((Throwable) result).getMessage());
	    log.error("Exception : ",(Throwable)result);
	} catch (NoSuchRemoteObjectException ex) {
	    // propagate NoSuchRemoteObjectExceptions generated by
	    // this.getEntry() to invoking client.
	    throw ex;
	} catch (Exception ex) {
	    log.warn("Something went wrong during method invocation : "
		    + ex.getMessage());
	    throw new TazioRuntimeException(ex);
	} finally {
	    synchronized (this.removalLock) {
		this.invoking--;
		// log.debug("Invocation " + (invocSeqNum++)
		// + " completed, notifying (current: " + this.invoking
		// + ")");
		this.removalLock.notifyAll();
	    }
	}

	synchronized (this.mbeanLock) {
	    this.executedInvocations++;
	}

	return result;
    }

    /**
     * This is an internal class used by the RemoteObjectsRegistry to store any
     * instance registered for remote method invocation along with every other
     * information related to it.
     * 
     * @version 0.0.1 Nov 12, 2009
     * @author Andrea Sorbini <as@mentalsmash.org>
     * 
     */
    class RegistryEntry {
	private final Object obj;
	private final OID oid;
	private final ProxyFixer fixer;

	// private final RemoteInvocationDispatcher dispatcher;

	public RegistryEntry(Object obj, OID oid, ProxyFixer fixer) {
	    this.obj = obj;
	    this.oid = oid;
	    this.fixer = fixer;
	    // this.dispatcher = dispatcher;
	}

	public Object getObject() {
	    return this.obj;
	}

	public OID getOID() {
	    return this.oid;
	}

	/**
	 * Creates a new {@link RemoteObject} proxy instance for the object
	 * contained in this entry.
	 * 
	 * @return the proxy instance created
	 */
	public RemoteObject getRemoteObject() {
	    RemoteObjectProxy proxy = new BaseRemoteObjectProxy(this.oid, hostOID,
		    this.obj);

	    // RemoteInvocationDispatcher dispatcher = this.invoker
	    // .getDispatcher();
	    // RemoteObjectsRegistry registry = this.invoker.getRegistry();
	    //
	    // proxy.setRegistry(registry);
	    // proxy.setDispatcher(dispatcher);

	    this.fixer.fix(proxy);

	    Class<?>[] interfaces = BaseRemoteObjectProxy
		    .getObjectInterfaces(this.obj);

	    RemoteObject ro = (RemoteObject) Proxy.newProxyInstance(this
		    .getClass().getClassLoader(), interfaces, proxy);

	    return ro;
	}

	public boolean hasMethod(RemoteMethod m) {
	    for (RemoteMethod rm : BaseRemoteObjectProxy
		    .getObjectMethods(this.obj)) {
		if (rm.equals(m))
		    return true;
	    }

	    return false;
	}

	@Override
	public String toString() {
	    return "[OID=\"" + this.oid + "\";Object=\"" + this.obj + "\"]";
	}

    }

    /*
     * (non-Javadoc)
     * 
     * @seeorg.mentalsmash.tazio.net.ril.RemoteObjectsRegistryMBean#
     * getLocalMethodInvocationsNum()
     */
    @Override
    public int getLocalMethodInvocationsNum() {
	synchronized (mbeanLock) {
	    return this.executedInvocations;
	}
    }

    /*
     * (non-Javadoc)
     * 
     * @seeorg.mentalsmash.tazio.net.ril.RemoteObjectsRegistryMBean#
     * getRegisteredObjectsNum()
     */
    @Override
    public int getRegisteredObjectsNum() {
	synchronized (this.entries) {
	    return this.entries.size();
	}
    }

    /**
     * This method is used to inject an MBeanServer instance that will be used
     * to register this MBean
     */
    @Inject
    public void register(MBeanServer server) {
	try {
	    
	    this.mbeanName = new ObjectName(
		    "org.mentalsmash.tazio.net.ril:type="
		    + "RemoteObjectsRegistry");
	    server.registerMBean(this, this.mbeanName);
	} catch (JMException e) {
	    System.err.println("exception: " + e.getClass().getCanonicalName()
		    + " " + e.getMessage());
	}
    }

    @Override
    public double getAvgInvocationTime() {
	System.out.println("Looking for : "+TLOG_KEY_INVOKE);
	return this.tlog.getAvgMeasure(TLOG_KEY_INVOKE);
    }

    @Override
    public long getMaxInvocationTime() {
	return this.tlog.getMaxMeasure(TLOG_KEY_INVOKE);
    }

    @Override
    public long getMinInvocationTime() {
	return this.tlog.getMinMeasure(TLOG_KEY_INVOKE);
    }
}
